<?php
//
// +------------------------------------------------------------------------+
// | Gecko Framework                                                        |
// +------------------------------------------------------------------------+
//
require_once('Gecko/Form/Exception.php');

/**
 * A web Form implementation, handles server validation and client
 * validation
 *
 * @package Gecko;
 * @author Christopher Valderrama <valderrama.christopher@gmail.com>
 * @copyright Copyright (c) 2008
 * @version $Id$2.0
 **/
class Gecko_Form {
	/**
	 * Class Constants
	 **/
	const METHOD_POST = 'post';
	const METHOD_GET = 'get';
	/**
	 * Form Name
	 **/
	private $name;

	/**
	 * Form Settings
	 **/
	private $settings;

	/**
	 * Form Fields
	 **/
	private $fields = array();

	/**
	 * Error Messages from validators
	 **/
	private $fieldMessages = array();

	/**
	 * The form default values
	 *
	 * @var array
	 */
	private $fieldDefaultValues = array();

	/**
	 * Form Properties
	 **/
	private $useJavascript = true; // Use Javascript
	private $formValid = false; // Form state
	private $usingFiles = false; // Check if using files (for correct encoding type)
	private $method = ''; // The form method (default's to POST)
	private $destination = ''; // The form destination (default's to self)

	/**
	 * Form decorator
	 **/
	private $formDecorator = null;

	/**
	 * To return readonly values
	 *
	 * @var boolean
	 */
	private static $returnReadOnly = false;

	/**
	 * Fields considered read-only, by default, submit, reset,
	 * and label elements
	 *
	 * @var array
	 */
	protected $readOnlyFields = array(
		'label',
		'submit',
		'reset'
	);

	/**
	 * Tells the form to return readonly values
	 *
	 * @param unknown_type $bool_return
	 */
	public static function returnReadOnly( $bool_return ) {
		self::$returnReadOnly = (boolean) $bool_return;
	}

	/**
	 * Shorthand for creating a field
	 *
	 * @param string $type The type of the field
	 * @param string $name The name of the field
	 * @param string $value The value of the field
	 * @param array $settings The settings for the field
	 * @return Gecko_Form_Field_Interface A field created
	 */
	public static function fieldFactory( $type, $name, $value = '', $settings = array() ) {
		return Gecko_Form_Field::factory($type, $name, $value, $settings);
	}

	/**
	 * Validator factory
	 *
	 * @param string $validator The validator name
	 * @return Object a Validator created
	 */
	public static function validatorFactory( $validator ) {
		$geckoValidator = "Gecko_Form_Validator_$validator";
		$zendValidator = "Zend_Validate_$validator";
		$isGecko = false;

		try {
			Zend_Loader::loadClass($geckoValidator);
			$isGecko = true;
		} catch( Zend_Exception $ze ) { // not a Gecko validator, check Zend
			Zend_Loader::loadClass($zendValidator);
		}

		if( $isGecko ) {
			$objValidator = new $geckoValidator();
		} else {
			$objValidator = new $zendValidator();
		}

		return $objValidator;
	}

	/**
	 * Creates a new Gecko_Form object from a builder, can be a XML Builder, DB Builder, etc.
	 *
	 * @see Gecko_Form_Builder_Interface
	 * @param Gecko_Form_Builder_Interface $builder
	 * @access public static
	 * @return Gecko_Form object created from builder
	 **/
	public static function createFromBuilder( Gecko_Form_Builder_Interface $builder ) {
		$settings = $builder->getFormSettings();
		$form = new self($settings);
		$fields = $builder->getFieldsSchema();
		foreach( $fields as $field ) {
			$form->addField( $field );
		}

		return $form;
	}

	/**
	 * Constructs a new Web Form with the specified settings
	 * like name, and destination
	 *
	 * @param array Array with settings
	 * @access public
	 * @return object
	 **/
	public function __construct($settings = array()) {
		if( isset( $settings['decorator'] ) && ( $settings['decorator'] instanceof Gecko_Form_Decorator_Interface ) ) {
			$decorator = $settings['decorator'];
		} else {
			$decorator = new Gecko_Form_Decorator_Basic();
		}

		if( !is_array( $settings ) ) {
			throw new Gecko_Form_Exception('$settings is expected to be an array, ' . gettype($settings) . ' given');
		}

		if(!isset($settings['name']) || empty($settings['name'])) {
			$name = uniqid("geckoform_");
		} else {
			$name = $settings['name'];
		}

		/**
		 * Check Form Method
		 */
		$method = self::METHOD_POST;
		if(isset( $settings['method'] ) && !empty( $settings['method'] ) ) {
			$method = $settings['method'];
		}

		/**
		 * Check if there are pre-set values
		 */
		if(isset( $settings['values'] ) && ( count( $settings['values'] ) > 0) ) {
			$this->populateForm($settings['values']);
		}

		/**
		 * Check if there is a setted destination for the form
		 */
		$destination = Gecko_URL::getSelfUri();
		if(isset( $settings['destination'] ) && !empty( $settings['destination'] ) ) {
			$destination = $settings['destination'];
		}

		$this->setName( $name );
		$this->setMethod( $method );
		$this->setDestination( $destination );
		$this->setFormDecorator( $decorator );

		/**
		 * Begin form
		 */
		$this->init();
	}

	/**
	 * Sets the form Name
	 *
	 * @param string $name
	 * @return Gecko_Form self object for chaining
	 */
	public function setName( $name ) {
		$this->name = $name;

		return $this;
	}

	/**
	 * Returns the form name
	 *
	 * @return string
	 */
	public function getName() {
		return $this->name;
	}

	/**
	 * Sets the form destination
	 *
	 * @param string The destination of the form
	 * @access public
	 * @return void
	 **/
	public function setDestination( $url ) {
		$this->destination = $url;

		return $this;
	}

	/**
	 * Returns the form destination
	 *
	 * @return string The form destination
	 */
	public function getDestination() {
		return $this->destination;
	}

	/**
	 * Sets the form method
	 *
	 * @param string The method of the form
	 * @access public
	 * @return void
	 **/
	public function setMethod( $method ) {
		if(!in_array( $method, array( self::METHOD_POST, self::METHOD_GET ) )) {
			throw new Gecko_Form_Exception($method . 'not suported use Gecko_Form::METHOD_GET, or Gecko_Form::METHOD_POST');
		}

		$this->method = $method;

		return $this;
	}

	/**
	 * Returns the method of the form
	 *
	 * @return string
	 */
	public function getMethod() {
		return $this->method;
	}

	/**
	 * Adds a new field to the form, it can be a field object (pre-created)
	 * or a array with field settings to add
	 *
	 * @param mixed The field or array
	 * @return Gecko_Form_Field Object The created or passed on object
	 * @access public
	 **/
	public function addField($field) {
		if($field instanceof Gecko_Form_Field_Interface ) {
			$name = $field->getName();
		} else {
			if(is_array($field)) {
				$type = $field['type'];
				$name = $field['name'];
				$value = $field['value'];
				$settings = $field['settings'];

				$field = Gecko_Form_Field::factory( $type, $name, $value, $settings );

				$validators = isset( $field['validators'] ) ? $field['validators'] : array();
				foreach( $validators as $validator ) {
					$geckoValidator = "Gecko_Form_Validator_$validator";
					$zendValidator = "Zend_Validate_$validator";
					$isGecko = false;

					try {
						Zend_Loader::loadClass($gekoValidator);
						$isGecko = true;
					} catch( Zend_Exception $ze ) { // not a Gecko validator, check Zend
						Zend_Loader::loadClass($zendValidator);
					}

					if( $isGecko ) {
						$objValidator = new $geckoValidator();
					} else {
						$objValidator = new $zendValidator();
					}

					$field->addValidator($objValidator);
				}


			} else {
				throw new Gecko_Form_Exception('$field expected to be a array or a Field object, ' . gettype($field) . ' given');
			}
		}

		$this->fields[$name] = $field;
		if( $field->getType() == "file" ) {
			$this->usingFiles = true;
		}

		return $field;
	}

	/**
	 * Returns the specified field
	 *
	 * @param string The field name
	 * @throws Gecko_Form_Exception if field isn't in the collection
	 * @access public
	 **/
	public function getField($name) {
		$field = $this->fields[$name];
		if( !$field ) {
			throw new Gecko_Form_Exception( "Field $field not found in collection" );
		}

		return $field;
	}

	/**
	 * Returns the fields from the form
	 *
	 * @access public
	 * @return array
	 **/
	public function getFields() {
		return $this->fields;
	}

	/**
	 * Returns the field data as an array
	 *
	 * @access public
	 * @return array
	 * @throw Gecko_Form_Exception if field is invalid
	 **/
	public function getData() {
		if( !$this->formValid ) {
			throw new Gecko_Form_Exception('Form is invalid, or not validated, use Gecko_Form::isValid() to validate');
		}

		$data = array();
		foreach( $this->fields as $name => $field ) {
			if( in_array( $field->getType(), $this->readOnlyFields ) ) { // is readonly
				if( self::$returnReadOnly === true ) { // if we are returning read-only
					$data[$name] = $field->getValue();
				}
			} else {
				$data[$name] = $field->getValue();
			}
		}

		return $data;
	}

	/**
	 * Enables or Disables the use of Javascript
	 *
	 * @param boolean User or not Javascript
	 * @access public
	 * @return void
	 **/
	public function setJavascript($boolUse = true) {
		$this->useJavascript = (boolean) $boolUse;
	}

	/**
	 * Adds a new label to the form
	 *
	 * Internally creates a field type label and adds it
	 * to the form
	 *
	 * @param string Label name
	 * @param string Label value
	 * @access public
	 * @return void
	 **/
	public function addLabel($name, $value) {
		$label = Gecko_Form_Field::factory( "label", $name, $value );

		$this->fields[$name] = $label;
	}

	/**
	 * Sets the default form decorator for drawing fields,
	 * and errors
	 *
	 * @param Gecko_Form_Decorator_Interface $decorator, the decorator to set
	 * @access public
	 * @return void
	 **/
	public function setFormDecorator(Gecko_Form_Decorator_Interface $decorator) {
		$this->formDecorator = $decorator;
	}

	/**
	 * Init function for form overloading, you can set
	 * form settings and field here
	 *
	 */
	protected function init() {

	}

	/**
	 * Returns the form renderer if none is submitted
	 *
	 * @return Gecko_Form_Renderer_Interface renderer
	 */
	protected function getFormRenderer() {
		require_once('Gecko/Form/Renderer/Table.php');

		return new Gecko_Form_Renderer_Table();
	}

	/**
	 * Populates a form with default data
	 *
	 * @param array $data The data to populate the form
	 */
	public function populateForm($data) {
		$this->fieldDefaultValues = $data;
	}

	/**
	 * Private function to fill default values
	 *
	 */
	private function fillFields() {
		foreach( $this->fields as $name => $field ) {
			if( isset( $this->fieldDefaultValues[$name] ) ) {
				$field->setValue( $this->fieldDefaultValues[$name] );
			}
		}
	}

	/**
	 * Returns the Source Values for the
	 * Form (Can be overriden to provide a custom DataSource)
	 *
	 * @return array The array to fetch values
	 */
	protected function getSourceValues() {
		$method = $this->getMethod();
		return (array) Gecko_Request::getRequest($method);
	}

	/**
	 * Loops through all fields and checks them for validity,
	 * if invalid invalidates form and sets correct error
	 * messages
	 *
	 * @access public
	 * @return boolean if form Valid
	 **/
	public function isValid() {
		$theValues = $this->getSourceValues();

		if( count( $theValues ) == 0 ) {
			return false;
		}

		// Asume form is valid
		$this->formValid = true;

		foreach( $this->fields as $name => $field ) {
			// label, submit, reset fields are skipped
			if( in_array( $field->getType(), $this->readOnlyFields ) ) {
				continue;
			}

			switch( $field->getType() ) {
				case 'file': // file fields require special functionality
					$value = $_FILES[$name];
					break;
				case 'checkbox': // checkbox arent sent so we must set default value
					if( !isset($postValues[$name] ) ) { // if no value then set to empty
						$settings = $field->getParams();
						if( isset( $settings['disabledValue'] ) ) {
							$value = $settings['disabledValue'];
						} else {
							$value = "";
						}
					} else {
						$value = $theValues[$name];
					}
					break;
				default: // all other controls are "standard"
					$value = $theValues[$name];
					break;
			}
			$field->setValue( $value );

			$result = $field->isValid($theValues);
			if( !$result->getResult() ) { // Invalid...
				$this->invalidateField( $name, $result->getMessages() );
			}
		}

		return $this->formValid;
	}

	/**
	 * Constructs and renders the web form, uses
	 * a Form Renderer to render data to the web page
	 *
	 * @see Gecko_Form_Renderer_Interface
	 * @param Gecko_Form_Renderer_Interface The renderer to use
	 * @access public
	 * @return The renderer with the form built
	 **/
	public function buildForm(Gecko_Form_Renderer_Interface $renderer = null) {
		/** Get some basic methods **/
		$name = $this->getName();
		$url = $this->getDestination();
		$enctype = ( $this->usingFiles == true ? 'multipart/form-data' : 'application/x-www-form-urlencoded' );
		$method = $this->getMethod();

		/**
		 * Check if we are submitting a renderer or get a default one
		 */
		if( $renderer === null ) {
			$renderer = $this->getFormRenderer();
		}

		$renderer->beginForm($name, $url, $method, $enctype);

		$this->fillFields();

		$js = array();
		$jsValidator = array();
		foreach( $this->fields as $field ) {
			$name = $field->getName();

			$html = $field->getControl();
			$js[] = $field->getJavascriptCode(); // This must be a valid JS
			$jsValidator[$name] = $field->getJavascriptValidationCode();
			$messages = (isset( $this->fieldMessages[$name] ) ? $this->fieldMessages[$name] : null );
			$renderer->addItem( $name, $this->formDecorator->decorateField($field), $messages );
		}

		$renderer->endForm();
		$jsValidator = $this->buildJSValidator($jsValidator);
		$renderer->addHTML( $jsValidator );
		$renderer->addHTML( implode( "\n", $js ) );

		Gecko_Template::useHelper( "prototype.js" );
		Gecko_Template::useHelper( "GeckoValidator.js" );

		return $renderer;
	}

	/**
	 * Creates the Javascript validator to inyect on the page
	 * it works with JSON and Gecko_Validator (javascript) to
	 * create plugins
	 *
	 * @param array Javascript code
	 * @access private
	 * @return string the javascript tag
	 **/
	private function buildJSValidator($js_code) {
		$validatorName = "js_validator_" . $this->getName();
		$strJavaScript = "var $validatorName = new GeckoValidator( \"". $this->getName() ."\" );\n";
		foreach( $js_code as $name => $code ) {
			if( count( $code ) == 0 ) continue;

			if( empty( $code ) ) continue;
			$strJavaScript.= "$validatorName.addField( \"$name\", $code );\n";
		}

		return Gecko_Template::getJavaScriptTag( $strJavaScript );
	}

	/**
	 * Invalidates a field in the form and sets error messages
	 *
	 * @param string The field name
	 * @param array With error messages
	 * @access private
	 * @return void
	 **/
	private function invalidateField( $field, $messages ) {
		$this->formValid = false;

		$this->fieldMessages[$field] = $this->formDecorator->decorateError( $messages );
	}
}